/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002-2006 * Sleepycat Software. All rights reserved. * * $Id: SecondaryDatabase.java,v 1.1 2006/05/06 08:59:29 ckaestne Exp $ */ package com.sleepycat.je; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.sleepycat.je.dbi.DatabaseImpl; import com.sleepycat.je.dbi.GetMode; import com.sleepycat.je.dbi.PutMode; import com.sleepycat.je.dbi.CursorImpl.SearchMode; import com.sleepycat.je.txn.Locker; import com.sleepycat.je.txn.LockerFactory; /** * Javadoc for this public class is generated via * the doc templates in the doc_src directory. */ public class SecondaryDatabase extends Database { private Database primaryDb; private SecondaryConfig secondaryConfig; private SecondaryTrigger secondaryTrigger; private ForeignKeyTrigger foreignKeyTrigger; /** * Creates a secondary database but does not open or fully initialize it. */ SecondaryDatabase(Environment env, SecondaryConfig secConfig, Database primaryDatabase) throws DatabaseException { super(env); DatabaseUtil.checkForNullParam(primaryDatabase, "primaryDatabase"); primaryDatabase.checkRequiredDbState(OPEN, "Can't use as primary:"); if (primaryDatabase.configuration.getSortedDuplicates()) { throw new IllegalArgumentException ("Duplicates must not be allowed for a primary database: " + primaryDatabase.getDebugName()); } if (env.getEnvironmentImpl() != primaryDatabase.getEnvironment().getEnvironmentImpl()) { throw new IllegalArgumentException ("Primary and secondary databases must be in the same" + " environment"); } if (secConfig.getKeyCreator() != null && secConfig.getMultiKeyCreator() != null) { throw new IllegalArgumentException ("secConfig.getKeyCreator() and getMultiKeyCreator() may not" + " both be non-null"); } if (!primaryDatabase.configuration.getReadOnly() && secConfig.getKeyCreator() == null && secConfig.getMultiKeyCreator() == null) { throw new NullPointerException ("secConfig and getKeyCreator()/getMultiKeyCreator()" + " may be null only if the primary database is read-only"); } if (secConfig.getForeignKeyNullifier() != null && secConfig.getForeignMultiKeyNullifier() != null) { throw new IllegalArgumentException ("secConfig.getForeignKeyNullifier() and" + " getForeignMultiKeyNullifier() may not both be non-null"); } if (secConfig.getForeignKeyDeleteAction() == ForeignKeyDeleteAction.NULLIFY && secConfig.getForeignKeyNullifier() == null && secConfig.getForeignMultiKeyNullifier() == null) { throw new NullPointerException ("ForeignKeyNullifier or ForeignMultiKeyNullifier must be" + " non-null when ForeignKeyDeleteAction is NULLIFY"); } if (secConfig.getForeignKeyNullifier() != null && secConfig.getMultiKeyCreator() != null) { throw new IllegalArgumentException ("ForeignKeyNullifier may not be used with" + " SecondaryMultiKeyCreator -- use" + " ForeignMultiKeyNullifier instead"); } if (secConfig.getForeignKeyDatabase() != null) { Database foreignDb = secConfig.getForeignKeyDatabase(); if (foreignDb.getDatabaseImpl().getSortedDuplicates()) { throw new IllegalArgumentException ("Duplicates must not be allowed for a foreign key " + " database: " + foreignDb.getDebugName()); } } primaryDb = primaryDatabase; secondaryTrigger = new SecondaryTrigger(this); if (secConfig.getForeignKeyDatabase() != null) { foreignKeyTrigger = new ForeignKeyTrigger(this); } } /** * Create a database, called by Environment */ void initNew(Environment env, Locker locker, String databaseName, DatabaseConfig dbConfig) throws DatabaseException { super.initNew(env, locker, databaseName, dbConfig); init(locker); } /** * Open a database, called by Environment */ void initExisting(Environment env, Locker locker, DatabaseImpl database, DatabaseConfig dbConfig) throws DatabaseException { /* Disallow one secondary associated with two different primaries. */ Database otherPriDb = database.findPrimaryDatabase(); if (otherPriDb != null && otherPriDb.getDatabaseImpl() != primaryDb.getDatabaseImpl()) { throw new IllegalArgumentException ("Secondary is already associated with a different primary: " + otherPriDb.getDebugName()); } super.initExisting(env, locker, database, dbConfig); init(locker); } /** * Adds secondary to primary's list, and populates the secondary if needed. */ private void init(Locker locker) throws DatabaseException { trace(Level.FINEST, "SecondaryDatabase open"); secondaryConfig = (SecondaryConfig) configuration; /* Insert foreign key triggers at the front of the list and append * secondary triggers at the end, so that ForeignKeyDeleteAction.ABORT * is applied before deleting the secondary keys. */ primaryDb.addTrigger(secondaryTrigger, false); Database foreignDb = secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null) { foreignDb.addTrigger(foreignKeyTrigger, true); } /* Populate secondary if requested and secondary is empty. */ if (secondaryConfig.getAllowPopulate()) { Cursor secCursor = null; Cursor priCursor = null; try { secCursor = new Cursor(this, locker, null); DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); OperationStatus status = secCursor.position(key, data, LockMode.DEFAULT, true); if (status == OperationStatus.NOTFOUND) { /* Is empty, so populate */ priCursor = new Cursor(primaryDb, locker, null); status = priCursor.position(key, data, LockMode.DEFAULT, true); while (status == OperationStatus.SUCCESS) { updateSecondary(locker, secCursor, key, null, data); status = priCursor.retrieveNext(key, data, LockMode.DEFAULT, GetMode.NEXT); } } } finally { if (secCursor != null) { secCursor.close(); } if (priCursor != null) { priCursor.close(); } } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public synchronized void close() throws DatabaseException { if (primaryDb != null && secondaryTrigger != null) { primaryDb.removeTrigger(secondaryTrigger); } Database foreignDb = secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null && foreignKeyTrigger != null) { foreignDb.removeTrigger(foreignKeyTrigger); } super.close(); } /** * Should be called by the secondaryTrigger while holding a write lock on * the trigger list. */ void clearPrimary() { primaryDb = null; secondaryTrigger = null; } /** * Should be called by the foreignKeyTrigger while holding a write lock on * the trigger list. */ void clearForeignKeyTrigger() { foreignKeyTrigger = null; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public Database getPrimaryDatabase() throws DatabaseException { return primaryDb; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public SecondaryConfig getSecondaryConfig() throws DatabaseException { return (SecondaryConfig) getConfig(); } /** * Returns the secondary config without cloning, for internal use. */ public SecondaryConfig getPrivateSecondaryConfig() { return secondaryConfig; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public SecondaryCursor openSecondaryCursor(Transaction txn, CursorConfig cursorConfig) throws DatabaseException { return (SecondaryCursor) openCursor(txn, cursorConfig); } /** * Overrides Database method. */ Cursor newDbcInstance(Transaction txn, CursorConfig cursorConfig) throws DatabaseException { return new SecondaryCursor(this, txn, cursorConfig); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus delete(Transaction txn, DatabaseEntry key) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key, "key", true); checkRequiredDbState(OPEN, "Can't call SecondaryDatabase.delete:"); trace(Level.FINEST, "SecondaryDatabase.delete", txn, key, null, null); Locker locker = null; Cursor cursor = null; OperationStatus commitStatus = OperationStatus.NOTFOUND; try { locker = LockerFactory.getWritableLocker (envHandle, txn, isTransactional()); /* Read the primary key (the data of a secondary). */ cursor = new Cursor(this, locker, null); DatabaseEntry pKey = new DatabaseEntry(); OperationStatus searchStatus = cursor.search(key, pKey, LockMode.RMW, SearchMode.SET); /* Delete the primary and all secondaries (including this one). */ if (searchStatus == OperationStatus.SUCCESS) { commitStatus = primaryDb.deleteInternal(locker, pKey); } return commitStatus; } finally { if (cursor != null) { cursor.close(); } if (locker != null) { locker.operationEnd(commitStatus); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus get(Transaction txn, DatabaseEntry key, DatabaseEntry data, LockMode lockMode) throws DatabaseException { return get(txn, key, new DatabaseEntry(), data, lockMode); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus get(Transaction txn, DatabaseEntry key, DatabaseEntry pKey, DatabaseEntry data, LockMode lockMode) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key, "key", true); DatabaseUtil.checkForNullDbt(pKey, "pKey", false); DatabaseUtil.checkForNullDbt(data, "data", false); checkRequiredDbState(OPEN, "Can't call SecondaryDatabase.get:"); trace(Level.FINEST, "SecondaryDatabase.get", txn, key, null, lockMode); CursorConfig cursorConfig = CursorConfig.DEFAULT; if (lockMode == LockMode.READ_COMMITTED) { cursorConfig = CursorConfig.READ_COMMITTED; lockMode = null; } SecondaryCursor cursor = null; try { cursor = new SecondaryCursor(this, txn, cursorConfig); return cursor.search(key, pKey, data, lockMode, SearchMode.SET); } finally { if (cursor != null) { cursor.close(); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus getSearchBoth(Transaction txn, DatabaseEntry key, DatabaseEntry data, LockMode lockMode) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus getSearchBoth(Transaction txn, DatabaseEntry key, DatabaseEntry pKey, DatabaseEntry data, LockMode lockMode) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key, "key", true); DatabaseUtil.checkForNullDbt(pKey, "pKey", true); DatabaseUtil.checkForNullDbt(data, "data", false); checkRequiredDbState(OPEN, "Can't call SecondaryDatabase.getSearchBoth:"); trace(Level.FINEST, "SecondaryDatabase.getSearchBoth", txn, key, data, lockMode); CursorConfig cursorConfig = CursorConfig.DEFAULT; if (lockMode == LockMode.READ_COMMITTED) { cursorConfig = CursorConfig.READ_COMMITTED; lockMode = null; } SecondaryCursor cursor = null; try { cursor = new SecondaryCursor(this, txn, cursorConfig); return cursor.search(key, pKey, data, lockMode, SearchMode.BOTH); } finally { if (cursor != null) { cursor.close(); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus put(Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus putNoOverwrite(Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus putNoDupData(Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public JoinCursor join(Cursor[] cursors, JoinConfig config) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. * @deprecated */ public int truncate(Transaction txn, boolean countRecords) throws DatabaseException { throw notAllowedException(); } /** * Updates a single secondary when a put() or delete() is performed on * the primary. * * @param locker the internal locker. * * @param cursor secondary cursor to use, or null if this method should * open and close a cursor if one is needed. * * @param priKey the primary key. * * @param oldData the primary data before the change, or null if the record * did not previously exist. * * @param newData the primary data after the change, or null if the record * has been deleted. */ void updateSecondary(Locker locker, Cursor cursor, DatabaseEntry priKey, DatabaseEntry oldData, DatabaseEntry newData) throws DatabaseException { /* * If we're updating the primary and the secondary key cannot be * changed, optimize for that case by doing nothing. */ if (secondaryConfig.getImmutableSecondaryKey() && oldData != null && newData != null) { return; } SecondaryKeyCreator keyCreator = secondaryConfig.getKeyCreator(); if (keyCreator != null) { /* Each primary record may have a single secondary key. */ assert secondaryConfig.getMultiKeyCreator() == null; /* Get old and new secondary keys. */ DatabaseEntry oldSecKey = null; if (oldData != null) { oldSecKey = new DatabaseEntry(); if (!keyCreator.createSecondaryKey(this, priKey, oldData, oldSecKey)) { oldSecKey = null; } } DatabaseEntry newSecKey = null; if (newData != null) { newSecKey = new DatabaseEntry(); if (!keyCreator.createSecondaryKey(this, priKey, newData, newSecKey)) { newSecKey = null; } } /* Update secondary if old and new keys are unequal. */ if ((oldSecKey != null && !oldSecKey.equals(newSecKey)) || (newSecKey != null && !newSecKey.equals(oldSecKey))) { boolean localCursor = (cursor == null); if (localCursor) { cursor = new Cursor(this, locker, null); } try { /* Delete the old key. */ if (oldSecKey != null) { deleteKey(cursor, priKey, oldSecKey); } /* Insert the new key. */ if (newSecKey != null) { insertKey(locker, cursor, priKey, newSecKey); } } finally { if (localCursor && cursor != null) { cursor.close(); } } } } else { /* Each primary record may have multiple secondary keys. */ SecondaryMultiKeyCreator multiKeyCreator = secondaryConfig.getMultiKeyCreator(); assert multiKeyCreator != null; /* Get old and new secondary keys. */ Set oldKeys = Collections.EMPTY_SET; Set newKeys = Collections.EMPTY_SET; if (oldData != null) { oldKeys = new HashSet(); multiKeyCreator.createSecondaryKeys(this, priKey, oldData, oldKeys); } if (newData != null) { newKeys = new HashSet(); multiKeyCreator.createSecondaryKeys(this, priKey, newData, newKeys); } /* Update the secondary if there is a difference. */ if (!oldKeys.equals(newKeys)) { boolean localCursor = (cursor == null); if (localCursor) { cursor = new Cursor(this, locker, null); } try { /* Delete old keys that are no longer present. */ Set oldKeysCopy = oldKeys; if (oldKeys != Collections.EMPTY_SET) { oldKeysCopy = new HashSet(oldKeys); oldKeys.removeAll(newKeys); for (Iterator i = oldKeys.iterator(); i.hasNext();) { DatabaseEntry oldKey = (DatabaseEntry) i.next(); deleteKey(cursor, priKey, oldKey); } } /* Insert new keys that were not present before. */ if (newKeys != Collections.EMPTY_SET) { newKeys.removeAll(oldKeysCopy); for (Iterator i = newKeys.iterator(); i.hasNext();) { DatabaseEntry newKey = (DatabaseEntry) i.next(); insertKey(locker, cursor, priKey, newKey); } } } finally { if (localCursor && cursor != null) { cursor.close(); } } } } } /** * Deletes an old secondary key. */ private void deleteKey(Cursor cursor, DatabaseEntry priKey, DatabaseEntry oldSecKey) throws DatabaseException { OperationStatus status = cursor.search(oldSecKey, priKey, LockMode.RMW, SearchMode.BOTH); if (status == OperationStatus.SUCCESS) { cursor.deleteInternal(); } else { throw new DatabaseException ("Secondary " + getDebugName() + " is corrupt: the primary record contains a key" + " that is not present in the secondary"); } } /** * Inserts a new secondary key. */ private void insertKey(Locker locker, Cursor cursor, DatabaseEntry priKey, DatabaseEntry newSecKey) throws DatabaseException { /* Check for the existence of a foreign key. */ Database foreignDb = secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null) { Cursor foreignCursor = null; try { foreignCursor = new Cursor(foreignDb, locker, null); DatabaseEntry tmpData = new DatabaseEntry(); OperationStatus status = foreignCursor.search(newSecKey, tmpData, LockMode.DEFAULT, SearchMode.SET); if (status != OperationStatus.SUCCESS) { throw new DatabaseException ("Secondary " + getDebugName() + " foreign key not allowed: it is not" + " present in the foreign database"); } } finally { if (foreignCursor != null) { foreignCursor.close(); } } } /* Insert the new key. */ OperationStatus status; if (configuration.getSortedDuplicates()) { status = cursor.putInternal(newSecKey, priKey, PutMode.NODUP); } else { status = cursor.putInternal(newSecKey, priKey, PutMode.NOOVERWRITE); } if (status != OperationStatus.SUCCESS) { throw new DatabaseException ("Could not insert secondary key in " + getDebugName() + ' ' + status); } } /** * Called by the ForeignKeyTrigger when a record in the foreign database is * deleted. * * @param secKey is the primary key of the foreign database, which is the * secondary key (ordinary key) of this secondary database. */ void onForeignKeyDelete(Locker locker, DatabaseEntry secKey) throws DatabaseException { ForeignKeyDeleteAction deleteAction = secondaryConfig.getForeignKeyDeleteAction(); /* Use RMW if we're going to be deleting the secondary records. */ LockMode lockMode = (deleteAction == ForeignKeyDeleteAction.ABORT) ? LockMode.DEFAULT : LockMode.RMW; /* * Use the deleted foreign primary key to read the data of this * database, which is the associated primary's key. */ DatabaseEntry priKey = new DatabaseEntry(); Cursor cursor = null; OperationStatus status; try { cursor = new Cursor(this, locker, null); status = cursor.search(secKey, priKey, lockMode, SearchMode.SET); while (status == OperationStatus.SUCCESS) { if (deleteAction == ForeignKeyDeleteAction.ABORT) { /* * ABORT - throw an exception to cause the user to abort * the transaction. */ throw new DatabaseException ("Secondary " + getDebugName() + " refers to a foreign key that has been deleted" + " (ForeignKeyDeleteAction.ABORT)"); } else if (deleteAction == ForeignKeyDeleteAction.CASCADE) { /* * CASCADE - delete the associated primary record. */ Cursor priCursor = null; try { DatabaseEntry data = new DatabaseEntry(); priCursor = new Cursor(primaryDb, locker, null); status = priCursor.search(priKey, data, LockMode.RMW, SearchMode.SET); if (status == OperationStatus.SUCCESS) { priCursor.delete(); } else { throw secondaryCorruptException(); } } finally { if (priCursor != null) { priCursor.close(); } } } else if (deleteAction == ForeignKeyDeleteAction.NULLIFY) { /* * NULLIFY - set the secondary key to null in the * associated primary record. */ Cursor priCursor = null; try { DatabaseEntry data = new DatabaseEntry(); priCursor = new Cursor(primaryDb, locker, null); status = priCursor.search(priKey, data, LockMode.RMW, SearchMode.SET); if (status == OperationStatus.SUCCESS) { ForeignMultiKeyNullifier multiNullifier = secondaryConfig.getForeignMultiKeyNullifier(); if (multiNullifier != null) { if (multiNullifier.nullifyForeignKey (this, priKey, data, secKey)) { priCursor.putCurrent(data); } } else { ForeignKeyNullifier nullifier = secondaryConfig.getForeignKeyNullifier(); if (nullifier.nullifyForeignKey (this, data)) { priCursor.putCurrent(data); } } } else { throw secondaryCorruptException(); } } finally { if (priCursor != null) { priCursor.close(); } } } else { /* Should never occur. */ throw new IllegalStateException(); } status = cursor.retrieveNext(secKey, priKey, LockMode.DEFAULT, GetMode.NEXT_DUP); } } finally { if (cursor != null) { cursor.close(); } } } DatabaseException secondaryCorruptException() throws DatabaseException { throw new DatabaseException ("Secondary " + getDebugName() + " is corrupt: it refers" + " to a missing key in the primary database"); } static UnsupportedOperationException notAllowedException() { throw new UnsupportedOperationException ("Operation not allowed on a secondary"); } /** * Send trace messages to the java.util.logger. Don't rely on the * logger alone to conditionalize whether we send this message, * we don't even want to construct the message if the level is * not enabled. */ void trace(Level level, String methodName) throws DatabaseException { Logger logger = envHandle.getEnvironmentImpl().getLogger(); if (logger.isLoggable(level)) { StringBuffer sb = new StringBuffer(); sb.append(methodName); sb.append(" name=").append(getDebugName()); sb.append(" primary=").append(primaryDb.getDebugName()); logger.log(level, sb.toString()); } } }